<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>大球吃小球 - 道具版</title>
  <style>
    html, body {
      margin: 0;
      padding: 0;
      overflow: hidden;
      background: #fff;
      width: 100vw;
      height: 100vh;
    }
    canvas {
      display: block;
    }
  </style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
// 获得2d图像的绘图工具
const ctx = canvas.getContext('2d');
let width = window.innerWidth;
// 获取浏览器窗口的内部宽度
let height = window.innerHeight;
// 获取浏览器窗口的内部高度
canvas.width = width;
canvas.height = height;

const NODE_RADIUS = 4; // 节点半径
const LINE_DIST = 160; // 线距离
const CIRCLE_RADIUS = 90; // 鼠标圆圈半径

const MAP_WIDTH = width * 5;
const MAP_HEIGHT = height * 5;
const INITIAL_NODE_COUNT = 800;
const MAX_NODES = 1000;
const AI_COUNT = 15;
const PLAYER_SPEED = 5;
const RADIUS_GROWTH_FACTOR = 0.5;
const POWERUP_LIFESPAN = 20000; // 20 seconds
const POWERUP_DURATION = 10000; // 10 seconds
const MAX_POWERUPS = 10;

let nodes = []; // 节点数组
let attractors = []; // Attractor array (player and AIs)
let powerUps = [];
let camX = 0;
let camY = 0;
let nextAiId = 1;

const keys = {
  w: false, a: false, s: false, d: false,
  ArrowUp: false, ArrowLeft: false, ArrowDown: false, ArrowRight: false
};

window.addEventListener('keydown', e => {
  if (Object.keys(keys).includes(e.key)) {
    e.preventDefault();
    keys[e.key] = true;
  }
});

window.addEventListener('keyup', e => {
  if (Object.keys(keys).includes(e.key)) {
    e.preventDefault();
    keys[e.key] = false;
  }
});

class Attractor {
  constructor(x, y, isPlayer = false) {
    this.x = x;
    this.y = y;
    this.isPlayer = isPlayer;
    this.id = isPlayer ? 'Player' : `AI ${nextAiId++}`;
    this.radius = CIRCLE_RADIUS;
    this.capturedNodes = [];
    this.color = isPlayer ? 'rgba(0, 150, 255, 0.4)' : `hsla(${Math.random() * 360}, 100%, 50%, 0.4)`;
    this.vx = isPlayer ? 0 : (Math.random() - 0.5) * 2;
    this.vy = isPlayer ? 0 : (Math.random() - 0.5) * 2;
    
    this.shieldActive = false;
    this.shieldEndTime = 0;
    this.speedBoostActive = false;
    this.speedBoostEndTime = 0;
  }

  move() {
    if (this.isPlayer) return; // Player is moved by keyboard

    const currentSpeed = this.speedBoostActive ? 1.5 : 1;
    this.x += this.vx * currentSpeed;
    this.y += this.vy * currentSpeed;

    if (this.x < this.radius || this.x > MAP_WIDTH - this.radius) this.vx *= -1;
    if (this.y < this.radius || this.y > MAP_HEIGHT - this.radius) this.vy *= -1;
  }
  
  draw() {
    ctx.save();
    
    // Draw the outline of the circle
    ctx.strokeStyle = this.color.replace('0.4', '0.8');
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.stroke();

    // Draw the node count in the center
    ctx.fillStyle = this.isPlayer ? '#000' : '#333';
    ctx.font = 'bold 24px Arial';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(this.capturedNodes.length, this.x, this.y);

    if (this.shieldActive) {
      ctx.strokeStyle = 'rgba(255, 255, 0, 1)'; // Bright yellow shield
      ctx.lineWidth = 5;
      ctx.beginPath();
      ctx.arc(this.x, this.y, this.radius + 8, 0, Math.PI * 2);
      ctx.stroke();
    }
    
    ctx.restore();
  }
}

class PowerUp {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.type = Math.random() < 0.5 ? 'speed' : 'shield';
    this.radius = 15;
    this.createdAt = Date.now();
    
    this.symbol = this.type === 'speed' ? 'S' : 'H';
    this.color = this.type === 'speed' ? 'rgba(100, 255, 100, 0.9)' : 'rgba(100, 100, 255, 0.9)';
  }

  draw() {
    ctx.save();
    ctx.fillStyle = this.color;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fill();

    ctx.fillStyle = '#000';
    ctx.font = 'bold 20px Arial';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(this.symbol, this.x, this.y);
    ctx.restore();
  }
}

function resetPlayer(player) {
  player.capturedNodes.forEach(node => {
    node.captor = null;
    node.vx = (Math.random() - 0.5) * 1.2;
    node.vy = (Math.random() - 0.5) * 1.2;
  });
  player.capturedNodes = [];
}

// 生成节点
function randomNode() {
  const hue = Math.random() * 360;
  return {
    x: Math.random() * MAP_WIDTH,
    y: Math.random() * MAP_HEIGHT,
    vx: (Math.random() - 0.5) * 1.2,
    vy: (Math.random() - 0.5) * 1.2,
    hue: hue,
    color: `hsla(${hue}, 80%, 60%, 0.85)`,
    captor: null, // New: who captured this node
    angle: 0,
    angularVelocity: 0,
    currentRadius: 0,
    targetRadius: 0
  };
}
// 添加随机生成节点的数量
for(let i=0; i<INITIAL_NODE_COUNT; i++) {
  nodes.push(randomNode());
}

// Create attractors
const playerAttractor = new Attractor(width / 2, height / 2, true);
attractors.push(playerAttractor);
for (let i = 0; i < AI_COUNT; i++) {
  attractors.push(new Attractor(Math.random() * MAP_WIDTH, Math.random() * MAP_HEIGHT));
}

// 动画主循环
function animate() {
  ctx.clearRect(0, 0, width, height);
  ctx.save();
  
  // --- Update Power-up Effects ---
  attractors.forEach(attractor => {
    if (attractor.speedBoostActive && Date.now() > attractor.speedBoostEndTime) {
      attractor.speedBoostActive = false;
    }
    if (attractor.shieldActive && Date.now() > attractor.shieldEndTime) {
      attractor.shieldActive = false;
    }
  });

  // --- Update attractor radius based on captured nodes ---
  attractors.forEach(attractor => {
    attractor.radius = CIRCLE_RADIUS + attractor.capturedNodes.length * RADIUS_GROWTH_FACTOR;
  });

  // --- AI Spawning ---
  const aiCount = attractors.filter(a => !a.isPlayer).length;
  if (aiCount < AI_COUNT) {
    attractors.push(new Attractor(Math.random() * MAP_WIDTH, Math.random() * MAP_HEIGHT));
  }

  // --- Power-up Spawning & Despawning ---
  powerUps = powerUps.filter(p => Date.now() - p.createdAt < POWERUP_LIFESPAN);
  if (powerUps.length < MAX_POWERUPS && Math.random() < 0.005) {
      powerUps.push(new PowerUp(Math.random() * MAP_WIDTH, Math.random() * MAP_HEIGHT));
  }
  
  // --- Player Keyboard Movement ---
  const currentSpeed = playerAttractor.speedBoostActive ? PLAYER_SPEED * 1.5 : PLAYER_SPEED;
  if (keys.w || keys.ArrowUp) playerAttractor.y -= currentSpeed;
  if (keys.s || keys.ArrowDown) playerAttractor.y += currentSpeed;
  if (keys.a || keys.ArrowLeft) playerAttractor.x -= currentSpeed;
  if (keys.d || keys.ArrowRight) playerAttractor.x += currentSpeed;
  
  // Clamp player position to map boundaries
  playerAttractor.x = Math.max(playerAttractor.radius, Math.min(playerAttractor.x, MAP_WIDTH - playerAttractor.radius));
  playerAttractor.y = Math.max(playerAttractor.radius, Math.min(playerAttractor.y, MAP_HEIGHT - playerAttractor.radius));
  
  // --- Set up canvas and camera ---
  camX = -playerAttractor.x + width / 2;
  camY = -playerAttractor.y + height / 2;
  ctx.translate(camX, camY);

  // --- Drawing background ---
  drawGrid();

  // --- Game Logic ---
  // Power-up Collection
  for (let i = powerUps.length - 1; i >= 0; i--) {
    const powerUp = powerUps[i];
    for (const attractor of attractors) {
      const dx = attractor.x - powerUp.x;
      const dy = attractor.y - powerUp.y;
      const dist = Math.sqrt(dx*dx + dy*dy);

      if (dist < attractor.radius + powerUp.radius) {
        if (powerUp.type === 'speed') {
          attractor.speedBoostActive = true;
          attractor.speedBoostEndTime = Date.now() + POWERUP_DURATION;
        } else if (powerUp.type === 'shield') {
          attractor.shieldActive = true;
          attractor.shieldEndTime = Date.now() + POWERUP_DURATION;
        }
        powerUps.splice(i, 1);
        break; // Power-up is collected, move to the next one
      }
    }
  }

  // Node generation
  const freeNodeCount = nodes.filter(n => !n.captor).length;
  const nodeDeficit = INITIAL_NODE_COUNT - freeNodeCount;
  const spawnProb = (nodeDeficit / INITIAL_NODE_COUNT) * 0.1;
  if (nodes.length < MAX_NODES && Math.random() < spawnProb) {
      nodes.push(randomNode());
  }

  // Handle attractors
  attractors.forEach(attractor => {
    attractor.move();
  });
  
  // Handle nodes
  nodes.forEach(node => {
    if (node.captor) {
      // It's captured, float around the captor
      node.angle += node.angularVelocity;
      node.currentRadius += (node.targetRadius - node.currentRadius) * 0.08;
      if (Math.random() < 0.01) {
        node.targetRadius = node.captor.radius + (Math.random() - 0.5) * 50;
      }
      node.x = node.captor.x + Math.cos(node.angle) * node.currentRadius;
      node.y = node.captor.y + Math.sin(node.angle) * node.currentRadius;
    } else {
      // It's free, check for capture and move
      for (const attractor of attractors) {
        const dx = node.x - attractor.x;
        const dy = node.y - attractor.y;
        const dist = Math.sqrt(dx * dx + dy * dy);

        if (dist < attractor.radius) {
          node.captor = attractor;
          attractor.capturedNodes.push(node);
          node.angle = Math.atan2(dy, dx);
          node.angularVelocity = (Math.random() - 0.5) * 0.04;
          node.currentRadius = dist;
          node.targetRadius = attractor.radius + (Math.random() - 0.5) * 50;
          break; // Captured by one, stop checking
        }
      }

      if (!node.captor) {
        node.x += node.vx;
        node.y += node.vy;
        // Boundary bounce for uncaptured nodes
        if (node.x < NODE_RADIUS || node.x > MAP_WIDTH - NODE_RADIUS) node.vx *= -1;
        if (node.y < NODE_RADIUS || node.y > MAP_HEIGHT - NODE_RADIUS) node.vy *= -1;
      }
    }
  });

  // Check for attractor collisions
  let remainingAttractors = [];
  for (let i = 0; i < attractors.length; i++) {
    let currentAttractor = attractors[i];
    if (!currentAttractor) continue;

    for (let j = i + 1; j < attractors.length; j++) {
      let otherAttractor = attractors[j];
      if (!otherAttractor) continue;

      const dx = currentAttractor.x - otherAttractor.x;
      const dy = currentAttractor.y - otherAttractor.y;
      const dist = Math.sqrt(dx * dx + dy * dy);

      if (dist < currentAttractor.radius + otherAttractor.radius) {
        const currentSize = currentAttractor.capturedNodes.length;
        const otherSize = otherAttractor.capturedNodes.length;
        
        let winner, loser;
        if (currentSize > otherSize) {
          winner = currentAttractor;
          loser = otherAttractor;
        } else if (otherSize > currentSize) {
          winner = otherAttractor;
          loser = currentAttractor;
        } else {
          continue; // Tie, nothing happens
        }

        // Shield check - a shielded player cannot lose
        if (loser.shieldActive) {
            continue;
        }

        // Transfer nodes
        loser.capturedNodes.forEach(node => {
          node.captor = winner;
          winner.capturedNodes.push(node);
        });
        
        if (loser.isPlayer) {
           resetPlayer(loser);
        } else {
           // Mark AI for removal
           attractors[j] = null; 
        }
      }
    }
    if (attractors[i]) {
      remainingAttractors.push(attractors[i]);
    }
  }
  attractors = remainingAttractors;

  // --- Drawing ---
  // 画线
  for(let i=0; i<nodes.length; i++) {
    for(let j=i+1; j<nodes.length; j++) {
      let dx = nodes[i].x - nodes[j].x;
      let dy = nodes[i].y - nodes[j].y;
      let dist = Math.sqrt(dx*dx + dy*dy);
      if(dist < LINE_DIST) {
        ctx.save();
        const alpha = 0.18 + 0.25 * (1 - dist / LINE_DIST);
        const hue = (nodes[i].hue + nodes[j].hue) / 2;
        ctx.strokeStyle = `hsla(${hue}, 80%, 60%, ${alpha})`;
        ctx.lineWidth = 1.2;
        ctx.beginPath();
        ctx.moveTo(nodes[i].x, nodes[i].y);
        ctx.lineTo(nodes[j].x, nodes[j].y);
        ctx.stroke();
        ctx.restore();
      }
    }
  }

  // Draw power-ups
  powerUps.forEach(p => p.draw());

  // Draw attractors
  attractors.forEach(attractor => attractor.draw());

  // 画节点
  nodes.forEach(node => {
    ctx.save();
    ctx.beginPath();
    ctx.arc(node.x, node.y, NODE_RADIUS, 0, Math.PI*2);
    ctx.fillStyle = node.color;
    ctx.shadowColor = 'rgba(255,0,0,0.5)';
    ctx.shadowBlur = 8;
    ctx.fill();
    ctx.restore();
  });

  ctx.restore();

  drawLeaderboard();

  requestAnimationFrame(animate);
}

function drawGrid() {
    ctx.save();
    ctx.strokeStyle = 'rgba(200, 200, 200, 0.2)';
    ctx.lineWidth = 1;
    const gridSize = 100;
    for (let x = 0; x <= MAP_WIDTH; x += gridSize) {
        ctx.beginPath();
        ctx.moveTo(x, 0);
        ctx.lineTo(x, MAP_HEIGHT);
        ctx.stroke();
    }
    for (let y = 0; y <= MAP_HEIGHT; y += gridSize) {
        ctx.beginPath();
        ctx.moveTo(0, y);
        ctx.lineTo(MAP_WIDTH, y);
        ctx.stroke();
    }
    ctx.restore();
}

function drawLeaderboard() {
  ctx.save();
  const sortedAttractors = [...attractors].sort((a, b) => b.capturedNodes.length - a.capturedNodes.length);
  const leaderboard = sortedAttractors.slice(0, 5);
  
  const startX = width - 220;
  const startY = 20;

  ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
  ctx.fillRect(startX - 10, startY - 10, 220, leaderboard.length * 28 + 20);

  ctx.font = 'bold 20px Arial';
  ctx.textAlign = 'left';
  ctx.textBaseline = 'top';
  
  ctx.fillStyle = '#FFF';
  ctx.fillText('Leaderboard', startX, startY);

  leaderboard.forEach((attractor, index) => {
    ctx.font = '18px Arial';
    if (attractor.isPlayer) {
      ctx.fillStyle = 'rgba(150, 200, 255, 1)'; // Highlight player
    } else {
      ctx.fillStyle = '#FFF';
    }
    const rank = index + 1;
    ctx.fillText(`${rank}. ${attractor.id}: ${attractor.capturedNodes.length}`, startX, startY + 30 + index * 25);
  });

  ctx.restore();
}

// 自适应窗口
window.addEventListener('resize', () => {
  width = window.innerWidth;
  height = window.innerHeight;
  canvas.width = width;
  canvas.height = height;
});

animate();
</script>
</body>
</html> 